/*
 * MIT License
 *
 * Copyright (c) 2025 milkpotatoes
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

export { };

enum RequireBaseDir {
    DEFAULT = 0,
    WORK_DIR = 1 << 0,
    SCRIPT_DIR = 1 << 1,
    BUILD_DIR = 1 << 2,
    EXECUTABLE_DIR = 1 << 3,
};

const nativeRequireNapi = exchangeInnerMethod<(path: string) => any>(globalThis, "requireNapi");
const nativePreLoad = exchangeInnerMethod<(path: string) => void>(globalThis, "preLoad");
const innerExecute = exchangeInnerMethod<(name: string, buffer: ArrayBuffer, entry: string) => boolean>(globalThis, "_innerExecute");
const buildDir = exchangeInnerMethod<string>(globalThis, "_buildDir");

const getFileAtStack = (stack: string): string => {
    const strs = stack.split('(');
    return strs[strs.length - 1].split(':')[0];
}

const getParent = (path: string): string => {
    const basePath = File.realPath(path);
    if (basePath === undefined) {
        return path;
    }
    const parent = basePath.substring(0, basePath.lastIndexOf('/') + 1);
    return parent;
}

const getAbsolutePath = (baseDir: string, file: string): string => {
    const absPath = File.realPath((baseDir.endsWith('/') ? baseDir : (baseDir + '/')) + file);
    return absPath ?? file;
}

const getNativeModulePath = (path: string, baseDir: RequireBaseDir | string): string => {
    if (path.startsWith('/')) {
        return path;
    }

    if (typeof baseDir === 'string') return getAbsolutePath(baseDir, path);

    if ((baseDir & RequireBaseDir.SCRIPT_DIR) !== 0) {
        const requireStack = new Error().stack!.split('\n')[2]!; // 2: skip current file and calling file
        const requireFile = File.realPath(getFileAtStack(requireStack));
        if (requireFile === undefined) return path;
        return getAbsolutePath(getParent(requireFile), path);
    }
    if ((baseDir & RequireBaseDir.WORK_DIR) !== 0) return getAbsolutePath(process.cwd(), path);
    if ((baseDir & RequireBaseDir.EXECUTABLE_DIR) !== 0) return getAbsolutePath(getParent(process.execPath), path);
    if ((baseDir & RequireBaseDir.BUILD_DIR) !== 0) return getAbsolutePath(buildDir, path);
    return path;
}

const defaultRequirePath = RequireBaseDir.WORK_DIR | RequireBaseDir.SCRIPT_DIR;
function requireNapi(path: string, baseDir: RequireBaseDir | string = RequireBaseDir.DEFAULT): any {
    return nativeRequireNapi(getNativeModulePath(path,
        baseDir === RequireBaseDir.DEFAULT ? defaultRequirePath : baseDir));
}

function preLoad(path: string, baseDir: RequireBaseDir | string = RequireBaseDir.DEFAULT): void {
    return nativePreLoad(getNativeModulePath(path,
        baseDir === RequireBaseDir.DEFAULT ? defaultRequirePath : baseDir));
}

const PANDA_ENTRY_POINT = '_GLOBAL::func_main_0';
function execute_string_ArrayBuffer(name: string, buffer: TextDecoderBuffer, entry: string): boolean {
    if (!(buffer instanceof ArrayBuffer)) {
        buffer = buffer.buffer;
    }
    return innerExecute(name, buffer, entry);
}

function execute_string_File(name: string, file: File, entry: string): boolean {
    const pos = file.tell();
    const buffer = file.read();
    file.seek(pos, File.SEEK_SET);
    if (!(buffer instanceof ArrayBuffer)) {
        throw new TypeError("Failed to execute 'execute' on 'globalThis', the 1st paramter must be 'BinaryMode'.");
    }
    return innerExecute(name, buffer, entry);
}

function execute(...args: any[]): boolean {
    if (arguments.length < 1) {
        throw new TypeError("Failed to execute 'execute' on 'globalThis': 1 argument required, but only 0 present.");
    }
    const [arg0, arg1, arg2 = PANDA_ENTRY_POINT] = args;
    let name;
    if (typeof arg0 === 'string') {
        name = arg0;
        if (ArrayBuffer.isView(arg1) || arg1 instanceof ArrayBuffer) {
            return execute_string_ArrayBuffer(name, arg1, String(arg2));
        } else if (arg1 instanceof File) {
            return execute_string_File(name, arg1, String(arg2));
        } else {
            throw new TypeError("Failed to execute 'execute' on 'globalThis', " +
                "the 2nd paramter must be 'TypedArray', 'DataView' or 'File'.");
        }
    } else {
        if (arg0 instanceof File) {
            return execute_string_File(arg0.path, arg0, String(arg1 ?? PANDA_ENTRY_POINT));
        }
        throw new TypeError("Failed to execute 'execute' on 'globalThis', " +
            "the 1st paramter must be 'string' or 'File'.")
    }
}

exportsToObject(globalThis,
    [
        ["requireNapi", requireNapi],
        ["preLoad", preLoad],
        ["execute", execute],
        ["RequireBaseDir", RequireBaseDir],
    ]
);